Conversation
Dashboard: - min/max value filters on Delegates (voting power) and Token Holders (balance) - Delegates as default tab; sidebar renamed to "Stakeholders" - larger rows with bottom borders, continuous activity ring, VP as percent of quorum - inactive-delegate flag and 0/0 states (Inactive / No proposals / Never voted) - inactive-VP alert banner on Token Holders; "Voted X/Y (Inactive)" on delegate column - clickable addresses that re-point the drawer; per-address Activity tab - Balance History In / Out / Vesting; dust badge and Hide dust switch on Top Interactions - VP History low-importance filter and All time; time selector MAX plus custom calendar range - delegate drawer tabs renamed (Voting Power, Delegation History); Former Delegators view - proposal final-result filter on the votes tab API: - new endpoints: voting-powers/inactive-summary, accounts/:address/delegators/historical, addresses/labels; address filter on feed/events; proposalStatusIn on proposals-activity Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address enrichment (ENS, contract flag, arkham labels) had no staleTime, and the global QueryClient defaults to 0, so every EnsAvatar/TypeCell refetched on each remount. Table re-renders (amplified by the inactive banner and per-row activity fetch) remounted rows and fired/canceled these requests repeatedly, flooding the address-enrichment API on every tab open. Give the useGetAddress and useGetAddresses calls a 5m staleTime / 30m gcTime so identical addresses dedupe and stay cached across remounts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- votes: rename user-vote filter labels For/Against to Yes/No - voting power: DS SegmentedControl for current/former view - voting power: summary (Current VP / Total VP Lost) on the selector row - former delegators: short date (Jan 3, 25), dedup VP impact when unchanged - delegation history: net VP change on graph, low-importance toggle on CSV row - activity: DS SegmentedControl for date and relevance - token holders: banner uses DS InlineAlert, table fills height below it - top interactions: hide-dust on CSV row, total as USD, Net Tokens In/Out (90D) - balance history: net balance change value on graph - period label: All time renamed to Max available data - table: footerActions slot; inline alert accepts ReactNode content Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- EnsAvatar: optional subtitle slot rendered under the name; the avatar stays vertically centered against the whole name + subtitle block - token holders: delegate column renders "Voted X/Y" via the subtitle slot so the avatar aligns with both lines - table: row dividers now live on the cells, since border-separate tables do not paint borders set on the <tr> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified against the Figma frame measured pixel by pixel, and against the locally rendered table: - delegate cell: avatar is vertically centered against the whole name + "Voted x/y" block (measured offset 0), and the subtitle starts at the same x as the name, as in the design - row borders: border-separate tables never paint borders declared on the <tr>, and the first cell additionally cleared them on desktop, so the divider was missing entirely and never reached the Address column. The line is now drawn by a cell pseudo element spanning the full cell width, ignoring the horizontal padding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- address column: long arkham labels are truncated instead of overflowing. The chain was broken by the tooltip trigger button, an inline-block whose shrink-to-fit width was set by the label, so `truncate` never had a width to work with. Clamping the trigger fixes every table using EnsAvatar. - avg vote timing: shows a skeleton while the per-row proposals activity loads, instead of the "-" it uses for delegates with no votes. - calendar popover: uses rounded-base, so the radius follows the DS token (0 for Anticapture, non-zero for whitelabels) instead of a hardcoded md. - token holders change column: right aligned, matching the delegates tab. - balance history and voting power graphs: the heading no longer changes between loading and loaded states. - drawer activity: the feed is scoped to the inspected wallet again. The address filter was fine; "All" omitted `relevance`, which the API reads as MEDIUM, so its value thresholds hid almost everything. The API now takes relevance=ALL to drop the threshold, and the drawer always sends the value explicitly. - drawer activity: infinite scroll works. The observer used the viewport as root while the list scrolls in its own container, leaving the sentinel on the clipped edge and the feed stuck on page one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ment - holders and delegates page title reads "Stakeholders", matching its nav entry - delegate votes: the rate metric card is labelled "For Rate" - former delegators: VP Impact header aligns left - voting power summary: the loading skeleton aligns left with the value it replaces Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An ENS record can be a 42 character address plus ".eth", which overflowed the address column and ran into the next one. DrawerAddressButton wraps the avatar in a button, and a button is inline-block, so it was sized by its content and the name below it never had a width to truncate against. Clamping it fixes every drawer table that renders an address. VP Impact is centered in both the header and the cells. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rated enum The previous guard compared against "ALL" directly, which only typechecks when the client happens to be generated from a spec that already exposes that value. CI regenerates the client against whichever Gateful it can reach, so the comparison broke there. Checking membership in the tiers this page offers works either way. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dule
Retry loop: useDelegatesActivity only recorded addresses whose fetch succeeded,
so a rejected (or empty) response left the address selectable again as soon as
it left the loading set. Since the effect depends on both sets, that refetched
the same addresses forever while the endpoint kept failing. Fetches now settle
into their own set, and one address failing no longer discards the others.
AAVE: its API registers no proposal endpoints, so the shared TokenHolders was
firing a 404 per delegate for proposals-activity and a 400 for the inactive
summary, which falls through to the /voting-powers/{address} param route. Both
are now gated on the DAO exposing proposal activity.
activityFromDate was recomputed from Date.now() on every render, so it changed
whenever a render crossed a second boundary and re-keyed both the per-delegate
activity cache and the banner query. Memoized on its inputs.
Total VP Lost summed only the pages already loaded while printing the API's
count of every former delegator beside it. It now claims a total only once
there is nothing left to load.
Canceled proposals are no longer votable in the inactive summary window, so it
agrees with proposals-activity instead of reporting a delegate as inactive for
skipping a vote that never happened.
Also: failed requests in the drawer activity feed and the former delegators
table no longer render as "nothing found"; the USD column shows a dash instead
of a confident "$0" while the token price is in flight; the Total Interactions
tooltip describes the value it actually shows; and an address inside
DrawerAddressButton no longer nests the tooltip trigger button inside the row
button, which cost a second tab stop and hijacked the accessible name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filter matched the stored proposals_onchain.status, but the service overwrites each proposal's status at read time with a derived value, and the indexer only ever persists ACTIVE, CANCELED, EXECUTED, PENDING, QUEUED and VETOED. DEFEATED, SUCCEEDED, NO_QUORUM, EXPIRED and PENDING_EXECUTION could therefore never match: on ENS "Failed" returned zero rows and zeroed all four metric cards, "Passed" silently dropped SUCCEEDED, and "Canceled" was unsatisfiable because the query already excludes canceled proposals. Making it correct means persisting the derived status from the indexer, or expressing the derivation in SQL over end block, vote tallies, quorum and timelock. That is its own task, so the filter and the proposalStatusIn param come out for now. The unrelated user vote filter on the same table stays. Also drops the "(90D)" from the Net Tokens In/Out label, which claimed a window the request never asked for. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gs link on its table The activity requests only ever sent fromDate, so with a custom range that ended in the past "Voted X/Y" counted every proposal from the range start up to today. The banner above the same table did send toDate, so the two disagreed about the window they described. Both hooks now send both bounds. Making Delegates the default tab broke the DAO overview entry point: the "Biggest holdings change" card links to holders-and-delegates with no tab param, so it landed on Delegates instead of the holders table it describes. It now asks for tokenHolders explicitly, like its delegate-side sibling already did. Also hardens the activity fetch in useDelegates the same way useDelegatesActivity was hardened. The retry loop there predates this PR, but promoting Delegates to the default tab makes it the first thing most visitors hit, so a failing proposals-activity endpoint would now hammer the API from the landing tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex: - Namespace the page-level custom range as rangeFrom/rangeTo. Plain from/to belong to the drawer's Balance History address filters, so a custom range was sending date strings as addresses and getting cleared on tab changes. - Collapse delegation rows per source event before sequencing former delegators. DAOs with partial delegation (SCR) write one row per delegatee out of a single DelegateChanged, sharing tx hash, log index and timestamp, so sibling delegates were read as moving away from each other. Only name a redelegation destination when the move-away event points a single delegation away from the queried address. - Move FeedEventItem into shared/. It also imported EntityType back from holders-and-delegates, so that type moves to shared/types/entities.ts, which clears two pre-existing violations in dao-overview too. - Drop the proposal final-result filter clause from the changeset; it was removed from this PR in 30daf70. isadorable: - Restore the "Holders & Delegates" page heading. Only the sidebar entry becomes "Stakeholders", per the DEV-562 decision. The subtitle prop that was meant to preserve it is dead: TheSectionLayout never rendered it, so drop it from the call sites and from the props type. - Replace the activity ring's hardcoded hex with stroke-border-contrast and stroke-success. The track was dark-mode --base-border, which rendered as dark grey on a light background. - Give the AAVE delegates table the DEV-476 min/max filter, and document why it is a reduced version of the shared Delegates table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
API: - proposals-activity accepts an optional `toDate`, enforced on the proposal timestamp in both the page and the analytics queries. The dashboard already sent it for custom ranges, where it was silently discarded, so bounded periods counted every proposal through today. - the feed's delegation enrichment keeps the row that mentions the filtered address. Partial delegations (SCR) write one row per delegatee out of a single DelegateChanged, all sharing tx hash and log index, so collapsing them by key could describe a delegate unrelated to the filtered address. Dashboard: - balance-change and voting-power-change totals read their period boundaries from their own limit-1 lookups instead of the plotted rows, which are capped at 1,000 and hide small events. Active accounts were reporting the change over a truncated suffix of the period. - "Hide dust" moves into the interactions query. Filtering client-side could empty a page, and an empty table drops the infinite-scroll sentinel, leaving qualifying rows on later pages unreachable. - per-address activity fetches carry a generation for the DAO and range, so a superseded response can no longer merge stale proposal counts into the rows. - the drawer's Activity tab only renders for DAOs whose API serves the feed; AAVE showed a permanent error state. An unknown tab in the URL now falls back to the first one instead of an empty body. - clicking an address in the drawer feed carries its entity type, so a delegate opens the delegate profile rather than the token-holder tabs. - the custom range calendar can apply a single day, via an explicit Apply. react-day-picker answers the first click with `from` equal to `to`, so the old inequality check made one specific day unselectable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…feat/holders-delegates-v3
…0860) Every in-drawer address column now states which kind of profile it points at, so DrawerAddressButton sets the drawerEntity override the same way the drawer's activity feed does. Clicking a delegator in a delegate drawer no longer opens that holder with delegate tabs, and clicking a delegate from a token holder's delegation history no longer keeps token holder tabs.
The holders and delegates tab param is parsed as an enum instead of a plain string, so a stale or hand-edited ?tab=foo coerces back to the default tab rather than missing every key in tabComponentMap and rendering an empty section body.
…eview #3675140866)
Partial delegation writes one delegations row per delegatee out of a single
DelegateChanged, so the primary row alone describes the event badly: an
unfiltered feed renders one arbitrary delegatee, and a feed filtered by the
delegator matches every sibling row, so picking one drops the others.
FeedDelegationMetadata gains an optional 'delegatees' array of
{ delegate, amount }, ordered by delegate address ascending and present only
when the event has more than one row. 'delegate', 'amount' and
'previousDelegate' keep their meaning and still come from the primary row that
indexDelegationsByKey selects, so the deployed client and the dashboard feed
renderer are unaffected.
…ages (review #3676245485) "Hide dust" is on by default and enforced by the query, so an address whose every interaction is under $1 came back empty and the early return took the whole table away, footer switch included, with no way to turn the filter back off. The table now stays mounted and shows an empty state that names the filter responsible. TopInteractions owns the genuinely-no-interactions case instead: its query carries no filters, so it hides the table and shows its blank slate alone rather than stacking two empty states.
…ew #3676245492) The override was a bare entity type, so any path that cleared drawerAddress without clearing it too (the section's tab cleanup, each parent's onClose) left it behind for the next address opened from a table, which then rendered the wrong profile's tabs. It is now recorded as '<entityType>:<address>' and honored only while the recorded address matches the drawer's, compared case-insensitively since the two come from different sources. Re-pointing the drawer drops the override by itself, so no cleanup path has to remember it, and a future one cannot reintroduce the bug. Encoding lives in a single useDrawerEntityOverride hook shared by both writers; DrawerActivityFeed writes through it directly, which retires the onEntityTypeChange prop that could not carry the address.
Drop comments that restated the code they sat on: JSX section labels (Filters, Timeline, Delegators), component headers that paraphrased the component name, and a guard comment narrating its own condition. Tighten the remaining ones to the reason the code is the way it is, and drop the trailing ticket refs, which point at the PR rather than at the code. Comments only, no behavior change.
…iew) The AAVE delegates table renders `combined - balance` as "Delegation received", but the repository applied `fromValue`/`toValue` to the combined total (delegated power plus the account's own balance). A large self balance alone could satisfy a minimum, and it could push a genuinely delegated account past a maximum. Filter the delegated power expression instead, which also matches what `orderBy=votingPower` already sorts by on the same endpoint, and what every other DAO does. Also guard the Top Interactions `minAmount`/`maxAmount` parse: both come from the URL, so a stale or hand-edited `?minAmount=1.5` threw inside `BigInt()` while rendering and took the drawer down. Invalid values are now ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex review (PR 2084, round 5) plus findings from a pass over the rest of the diff looking for the same defect classes. Bound votes by `toDate` in both activity aggregates. The window holds every proposal whose voting period overlaps the range, so a proposal opened near the end stays votable after it; a vote cast later was counting as activity inside a period that closed before the vote existed. The bound goes in the LEFT JOIN's ON clause, never in WHERE, so the proposal is still listed with no vote attached, which also keeps the `no_vote` filter and the voteTiming ordering consistent. `getUserVotes` takes the same bound so the analytics (votedProposals, winRate, yesRate, avgTimeBeforeEnd) agree with the page. The banner copy, "no votes cast in the selected period", is the semantics being enforced here. The lower bound is deliberately left off: the window includes proposals that opened before the range and were still votable inside it, and a vote on one of those is real participation, so bounding below would trade this overcount for an undercount. Self-review findings: - Voting Power History let a user minimum below 1 token replace the low importance floor instead of combining with it, so sub-token rows came back while the switch still read as on. It now takes the larger of the two, and ignores an unparseable URL value rather than letting it defeat the floor. - Delegates did not validate `drawerAddress` while its sibling Token Holders tab does, so a hand-edited value opened a drawer every address query below rejects. Both use `parseAsAddress` now. - The AAVE page parsed `tab` as a plain string, so `?tab=foo` rendered Token Holders with neither button highlighted. Enum parsed, which also retires the `as TabId` cast. This one predates the PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unifying the stored-args-to-calldata conversion put the encoder on storageToArg, which reads an unparseable array or tuple as an empty container. A draft that never passed ProposalFormSchema could publish valid calldata for an empty array while the action row described a call with arguments, with nothing to see either side of the publish. storageToArgStrict is now the conversion, refusing a blank, unparseable JSON, or JSON that is not an array; storageToArg is that plus the empty fallback, so the modal's live preview still renders while an array is half-typed and the two cannot drift. review: fail closed on malformed composite args (#3715934430) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oint The proposal list needs the new-proposal menu, the JSON import modal and the handoff that carries an imported proposal to the form. Those arrived as three more imports into create-proposal's internals, on top of the paths GovernanceSection already reached into. They go through the feature's existing barrel instead, so the governance side names one entry point and the internal layout stays movable. The two older internal paths are left alone: untangling them is a separate question from what this PR adds. review: move proposal import UI behind the feature barrel (#3715934434) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s in The proposals list stashed a pending import under the raw `daoId` route param while the creation form read it back lowercased, so opening `/ENS/proposals` stored under `ENS` and the form looked under `ens`. The import succeeded, the navigation happened, and the author landed on an empty form with nothing said. Normalize in `keyFor`, the one place both sides go through, rather than at a single call site, so the two stay in agreement whatever a caller passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ses it stashImportedProposal returns false when sessionStorage is blocked or full, so the caller can keep the author in the dialog with their document. The dialog defeated that: it called onImport and then closed unconditionally, and closing clears the textarea, so a storage failure showed a toast and took the hand-written JSON with it. onImport now reports whether it took the values, and the dialog closes only on a true. GovernanceSection returns the result of the stash, keeping its error toast on failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hold
storageToArgStrict is the encode path, but it stringified every leaf, so a
shared or API draft that never passed ProposalFormSchema could carry a
composite arg like [null] or [{}] and have it become "null" or
"[object Object]". customActionIssues reads that as a filled-in string leaf,
and encodeActions published a value nobody wrote instead of failing closed.
The strict conversion now walks the parsed value against the parameter:
only JSON scalars are leaves, a list is required wherever the ABI says one
goes, a value wherever it says a value goes, and a tuple may not carry more
entries than it has components. The lenient conversion still degrades to the
empty container, so the modal's live preview keeps rendering mid-edit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Validation parsed composite args with the forgiving converter while
encodeActions used the strict one, so the two could disagree. A leaf the ABI
cannot hold, `[{}]` or `[null]` for a `string[]`, or an object where a
`tuple[]` wants a tuple, parses as a JSON array and clears the shape checks;
the forgiving converter then degrades it to the empty container, which counts
as a complete dynamic array. Publish turned on and encodeActions threw on the
very same arg. Reachable from any draft that did not come through the import
converter, saved or shared.
customActionIssues now converts with storageToArgStrict, so whatever it
accepts is what argsToTreesStrict can encode. The composite pass ahead of it
stays, only to keep naming the two failures worth naming.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`coerceStrict` filled a missing tuple field with "", so a stored action holding `[]` for `tuple(string memo)` converted to `[""]` and published calldata carrying an empty string the draft never described. The row said one thing, the chain got another. A count that does not match is now refused in both directions, the same way an extra entry already was. The lenient `storageToArg` keeps its fallback, so a short tuple hydrates as the empty struct instead of a padded one; the modal writes full arity tuples back regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, both in the conversion layer between a stored arg and calldata. The safe conversion is now the default one. `storageToArg` and `argsToTrees` are the strict pair that refuses anything the ABI cannot hold, and the forgiving pair is `storageToArgForDisplay` and `argsToTreesForDisplay`, named for the only thing that answer is good for. Every bug in this file came from a caller reaching for the forgiving variant to decide whether an action was complete, so the plain name is the one that fails closed and the other has to be asked for. The custom action modal now draws its inputs from the display conversion and gates its Add button and calldata preview on the strict one, which it was not doing. And the utils are smaller. The JSON scanner loses its class and tracks only numbers, which are the only values the parser rejects for being unquoted, so it still reports the line and the digits as written for a third of the size. importIssueCopy was three string formatters with one consumer, so it lives in that consumer now. importedArgs loses the per-failure object literals to three helpers, without changing a message. Comments across all of them are cut back to invariants, from up to 75 percent of a file down to the repo's own range. scanJsonSource 316 to 153, plus 200 to 110 in its test importedArgs 299 to 215 importHandoff 76 to 49 address 29 to 11 importIssueCopy 149 to 0, deleted with its test No behaviour change beyond the modal gate: 463 tests pass, and the three that asserted forgiveness now ask for it by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`argsToTrees` mapped over the ABI's inputs alone, so a missing arg read as "" and an extra one was dropped. An action whose args and function drifted apart in a draft that never passed `ProposalFormSchema` therefore published calldata its row never described: `setMessage(string)` with `args: []` encoded an empty string instead of failing. Same fail-closed reasoning as the tuple arity check in `coerceStrict`, one level up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment described `storageToArg` as forgiving and referred to `argsToTreesStrict`; both names moved when the strict variants became the plain ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An import stashes before it navigates, because a sign-in can leave the page and the values have to survive it. Signed out, `goToNewProposal` only opened the sign-in modal, and dismissing it cleared the redirect while leaving the stash: the next "Create new" for that DAO in the tab drained it and filled a form that was asked to be blank, announcing an import the author had walked away from and arming the dirty flag NavigationGuard reads. `openLogin` now takes an `onDismiss`, so whoever staged for the post-sign-in route owns undoing it. Deferring the stash instead was not available: magic link and OAuth unload the page, which is why the handoff is sessionStorage rather than state. The undo is not gated on there being no session. `LoginProvider` reads `useSession` while its callers gate on `useAuthSession`, and the two disagree for a stale wallet session — which an open modal actively keeps alive, since `authFlowActive` stands the sign-out effect down. Gating would have skipped the undo in exactly the case the caller was signed out from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The magic-link sign-in mails an absolute URL back to the creation form, and following it from a mail client opens a new browsing context. sessionStorage is per-context, so `takeImportedProposal` found nothing there: the author signed in successfully and landed on a blank form with their document gone and nothing said. Surviving a sign-in that leaves the page is the whole job of this module, and only cross-context storage does it. The comment above `keyFor` had justified sessionStorage over the URL, which is a question about size, and that was mistaken for having settled the question about scope. localStorage costs what sessionStorage was buying: any tab can drain the stash, and it outlives the tab that wrote it. So the record now carries its write time and is refused after an hour — long enough for a mail round trip, short enough not to fill a form in a later session that nobody asked to fill — and is cleared whether it was accepted, expired, or malformed. Two tabs importing for the same DAO share the key and the last write wins, which is accepted and written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # apps/dashboard/features/create-proposal/schema.ts
…e style AGENTS.md asks for arrow functions; `createMemoryStorage` in the import handoff test was the one `function` declaration this PR adds, so it is converted. The other declarations in these files come from dev untouched and are left alone. Comment blocks were the bigger drift: 27 of them ran past 8 lines, against a ceiling of 3-8 in dev's own version of the same files. All 26 that this PR introduced are cut to the essential reason; the 16-line block in LoginProvider is dev's and stays as it is. Comments and one declaration keyword only — no behaviour change. 487 tests, typecheck and lint all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The JSON import restated rules the form already owned, and the parameter
walk they both need was written out at every call site. Three passes
answered "can this argument be encoded as what it claims to be" —
isArgComplete for the editor, tupleArityError for a better arity message,
and the import's own translation — and they disagreed on arity wording and
on whether an empty dynamic array counted as filled in.
- argIssues is now that single answer, and isArgComplete is it as a
boolean. tupleArityError and the composite pre-pass are gone.
- customActionIssues moves from ProposalFormSchema's superRefine onto
ProposalActionSchema, so the import dialog reaches it before the form
exists. A paste naming a function that isn't in its ABI is refused in
the dialog instead of turning up later as a Publish button that will
not enable.
- The import's parallel action schemas are derived from the form's
members (PendingProposalActionSchema) rather than kept in step by hand.
- shapeOf replaces the array/tuple/leaf preamble that ten walkers each
spelled out, and arityError is the one place a container's declared
size is compared with what it holds.
- Four identical {path, message} types collapse to one Issue.
The Solidity type grammar moves to abitype's zod schemas, which viem is
already built on: uint257, uint255, bytes33, bytes0, fixed128x18 and a
tuple with no components were all being caught by hand-written regexes.
Readability stays a separate, looser check, because judging it with the
grammar drops the offending function from the ABI list and loses the
message that names the bad type. `function` stays manual — legal ABI that
viem's encoder refuses.
Numbers no longer have to arrive quoted. The document is read through
jsonc-parser, so 1000000000000000001 and 1.000000000000000001 survive as
the text they were written as instead of as rounded doubles, which is what
the quoting rule existed to work around. That also retires the
hand-written source scanner: line numbers now come from the syntax tree,
for every path rather than only for figures.
One consequence worth naming: a number where text belongs is now read as
that text, so {"title": 42} imports a title of "42" rather than being
refused.
Both already resolved in the tree — abitype as viem's own dependency, jsonc-parser transitively — so this only declares them.
987 comment lines across the feature, 19% of everything the branch added. Most of it narrated the code beside it or recorded what the code used to be, which is review context with a shelf life, not something the next reader needs. What stays is the set of things that cost a bug or would cause one: a pasted `decimals` reaching `parseUnits`, raw calldata winning over `functionName` in the encoder, the modal's ticket against a stale decimals lookup, `storageToArg` vs the forgiving variant, the checksum-agnostic resolver, `function` being legal ABI that viem refuses, and why the document is not read with `JSON.parse`. One or two lines each, on the line they guard. Comments now 164 of 4,303 added lines (3%): 144 in production, 20 in tests, where a name already says what a case is for. No code changed — 521 tests pass, lint clean, tsc identical to baseline.
feat(dashboard): import a proposal from JSON when creating one
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6450e32cb1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ImportJsonModal, | ||
| NewProposalMenu, | ||
| clearImportedProposal, | ||
| stashImportedProposal, |
There was a problem hiding this comment.
Move proposal-import orchestration out of governance
Move the newly imported modal, menu, and local-storage handoff out of the governance feature. These imports couple governance directly to create-proposal internals, so either feature can no longer evolve independently; compose the flow at an app/widget boundary or move genuinely reusable orchestration into shared.
AGENTS.md reference: AGENTS.md:L100-L101
Useful? React with 👍 / 👎.
| size="md" | ||
| aria-haspopup="menu" | ||
| aria-expanded={open} | ||
| {...triggerProps} |
There was a problem hiding this comment.
Attach proposal-create telemetry to the selected action
When GovernanceSection supplies proposal_create_click through triggerProps, spreading those props onto the popover trigger records the event as soon as a user merely opens the menu. ConditionalPostHog captures the closest tagged element on that click, so cancellations and users who subsequently choose Import JSON inflate the proposal-creation funnel; put the existing event tags on the “Create new” menu item and give imports a distinct event if needed.
Useful? React with 👍 / 👎.
chore: version packages
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 139c236f70
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <AmountFilter | ||
| filterId="delegates-voting-power-filter" | ||
| sortOptions={AMOUNT_SORT_OPTIONS} |
There was a problem hiding this comment.
Synchronize amount-filter state with URL filters
The table query is driven by minValue/maxValue, but this AmountFilter initializes its fields from a separate persistent Zustand instance and receives none of those URL values. After applying a filter and switching tabs, cleanupFilters clears only the URL, so returning and pressing Apply unexpectedly restores the stale range; conversely, reloading or opening a shared filtered URL shows blank inputs despite an active filter. Initialize/reset the filter store from the query state so the displayed and applied values match the rows.
Useful? React with 👍 / 👎.
| // include the whole end day | ||
| toDate: | ||
| Math.floor(customRange.to.getTime() / 1000) + DAY_IN_SECONDS - 1, |
There was a problem hiding this comment.
Derive the custom end boundary from local midnight
For users in a DST-observing timezone who select an end date on a clock-change day, adding a fixed 86,400 seconds does not include exactly that local calendar day. A spring-forward end date includes roughly the first hour of the following day, while a fall-back date omits roughly the final hour, so transfers and delegate activity around that boundary appear in the wrong custom range. Compute the next local calendar midnight and subtract one second instead.
Useful? React with 👍 / 👎.
Note
Medium Risk
Changes SQL semantics for delegate activity and inactive VP across date windows; incorrect bounds could mislabel delegates inactive. New endpoints and feed filters are additive, but AAVE filter behavior is a breaking fix for clients that relied on combined VP ranges.
Overview
Holders & Delegates v3 adds API support for inactive delegated VP (
GET /voting-powers/inactive-summary), former delegators (GET /accounts/:address/delegators/historical), and per-DAO treasury/vesting labels (GET /addresses/labels). Proposal activity gains optionaltoDateand stricter window rules: proposals count when voting opens (creation + voting delay), and votes only count if cast inside[fromDate, toDate]. Feed acceptsrelevance=ALL, optionaladdressfiltering, and optionaldelegateeson split delegations. AAVEfromValue/toValueon voting powers now filter delegated power only.The dashboard renames the section to Stakeholders (
/stakeholderswith redirects fromholders-and-delegates), defaults the Delegates tab, and extends AAVE delegate tables with amount filters and row borders. Create-proposal description limit rises to 100,000 characters with pre-save validation. DAO overview updates Security Council copy (configurable label, ENS July 2026 council per changeset) and links point at the new stakeholders routes.Heavy unit test coverage backs former-delegator SQL, inactive-summary, proposals-activity date bounds, and feed address/split-delegation behavior.
Reviewed by Cursor Bugbot for commit d19b3d8. Configure here.